2.9.7. 将 C 编译为汇编,以及编译和链接汇编和 C 代码

编译器可以将 C 代码编译为汇编代码,并且可以将汇编代码编译为链接到二进制可执行程序的二进制形式。我们使用 IA32 汇编和“gcc”作为示例汇编语言和编译器,但任何 C 编译器都支持此功能,并且大多数编译器支持编译为多种不同的汇编语言。有关汇编代码和汇编编程的详细信息,请参阅[第 8 章](https://diveintosystems.org/book/C8-IA32/index.html#_ assembly_chapter)。

考虑这个非常简单的 C 程序:

simpleops.c

int main(void) {
    int x, y;
    x = 1;
    x = x + 2;
    x = x - 14;
    y = x*100;
    x = x + y * 6;

    return 0;
}

gcc 编译器将使用 -S 命令行选项将其编译为 IA32 程序集文本文件 (.s) 来指定编译为程序集,并使用 -m32 命令行选项来指定生成 IA32 程序集:

$ gcc -m32 -S simpleops.c   # runs the assembler to create a .s text file

此命令使用编译器对 C 代码的 IA32 汇编翻译创建一个名为simpleops.s的文件。由于.s文件是文本文件,因此用户可以使用任何文本编辑器查看(并编辑)它。例如:

$ vim simpleops.s

传递额外的编译器标志为gcc提供了指示,即它应该在将 C 转换为 IA32 汇编代码时使用某些功能或优化。

汇编代码文件,无论是从gcc生成的还是由程序员手工编写的,都可以使用-c选项由gcc编译为二进制机器代码形式:

$ gcc -m32 -c simpleops.s   # compiles to a relocatable object binary file (.o)

然后可以将生成的simpleops.o文件链接到二进制可执行文件(注意:这需要在您的系统上安装 32 位版本的系统库):

$ gcc -m32 -o simpleops simpleops.o  # creates a 32-bit executable file

此命令为 IA32(和 x86-64)架构创建一个二进制可执行文件simpleops

用于构建可执行文件的gcc命令行可以包含.o.c文件,这些文件将被编译并链接在一起以创建单个二进制可执行文件。

系统提供允许用户查看二进制文件的实用程序。例如,objdump显示.o文件中的机器代码和汇编代码映射关系:

$ objdump -d simpleops.o

可以将此输出与汇编文件进行比较:

$ cat simpleops.s

您应该看到类似这样的内容(我们用 C 程序中的相应代码注释了一些汇编代码):

        .file   "simpleops.c"
        .text
        .globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $16, %esp
        movl    $1, -8(%ebp)      # x = 1
        addl    $2, -8(%ebp)      # x = x + 2
        subl    $14, -8(%ebp)     # x = x - 14
        movl    -8(%ebp), %eax    # load x into R[%eax]
        imull   $100, %eax, %eax  # into R[%eax] store result of x*100
        movl    %eax, -4(%ebp)    # y = x*100
        movl    -4(%ebp), %edx
        movl    %edx, %eax
        addl    %eax, %eax
        addl    %edx, %eax
        addl    %eax, %eax
        addl    %eax, -8(%ebp)
        movl    $0, %eax
        leave
        ret
        .size   main, .-main
        .ident	"GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
        .section	.note.GNU-stack,"",@progbits

编写和编译汇编代码(Writing and Compiling Assembly Code)

程序员可以手动编写自己的汇编代码,并使用gcc将其编译成二进制可执行程序。例如,要在汇编中实现函数,请将代码添加到.s文件中并使用gcc对其进行编译。以下示例显示了 IA32 汇编中函数的基本结构。此类代码将写入具有原型int myfunc(int param);的函数的文件(例如myfunc.s)中。具有更多参数或需要更多空间用于局部变量的函数的前导码可能略有不同。

        .text                   # this file contains instruction code
.globl myfunc                   # myfunc is the name of a function
        .type   myfunc, @function
myfunc:                         # the start of the function
        pushl   %ebp            # function preamble:
        movl    %esp, %ebp      #  the 1st three instrs set up the stack
        subl    $16, %esp

        # A programmer adds specific IA32 instructions
        # here that allocate stack space for any local variables
        # and then implements code using parameters and locals to
        # perform the functionality of the myfunc function
        #
        # the return value should be stored in %eax before returning

        leave    # function return code
        ret

想要调用此函数的 C 程序需要包含其函数原型:

#include <stdio.h>

int myfunc(int param);

int main(void) {
    int ret;

    ret = myfunc(32);
    printf("myfunc(32) is %d\n", ret);

    return 0;
}

以下gcc命令从myfunc.smain.c源文件构建可执行文件(myprog):

$ gcc -m32 -c myfunc.s
$ gcc -m32 -o myprog myfunc.o main.c

手写汇编代码(handwritten assembly code)

与 C 语言不同,C 语言是一种可以在多种系统上编译和运行的高级语言,而汇编代码的级别非常低,并且特定于特定的硬件体系结构。程序员可以为低级函数或对其软件性能至关重要的代码序列手写汇编代码。程序员有时可以编写比编译器优化的 C 汇编语言运行速度更快的汇编代码,有时 C 程序员希望在其代码中访问底层体系结构的低级部分(例如特定寄存器)。由于这些原因,操作系统代码的一小部分通常用汇编代码实现。然而,由于C是一种可移植语言,并且比汇编语言高级得多,所以绝大多数操作系统代码都是用C编写的,依靠良好的优化编译器来产生性能良好的机器代码。

尽管大多数系统程序员很少编写汇编代码,但能够阅读和理解程序的汇编代码是更深入地了解程序的功能及其执行方式的一项重要技能。它还可以帮助了解程序的性能以及发现和理解程序中的安全漏洞。